import AppKit
import Commander
import Foundation
import PeekabooCore

protocol SpaceCommandSpaceService: Sendable {
    func getAllSpaces() async -> [SpaceInfo]
    func getSpacesForWindow(windowID: CGWindowID) async -> [SpaceInfo]
    func moveWindowToCurrentSpace(windowID: CGWindowID) async throws
    func moveWindowToSpace(windowID: CGWindowID, spaceID: CGSSpaceID) async throws
    func switchToSpace(_ spaceID: CGSSpaceID) async throws
}

enum SpaceCommandEnvironment {
    @TaskLocal
    private static var override: (any SpaceCommandSpaceService)?

    static var service: any SpaceCommandSpaceService {
        self.override ?? LiveSpaceService.shared
    }

    static func withSpaceService<T>(
        _ service: any SpaceCommandSpaceService,
        perform operation: () async throws -> T
    ) async rethrows -> T {
        try await self.$override.withValue(service) {
            try await operation()
        }
    }

    private final class LiveSpaceService: SpaceCommandSpaceService {
        static let shared = LiveSpaceService()
        @MainActor private static let actor = SpaceManagementActor()

        private init() {}

        func getAllSpaces() async -> [SpaceInfo] {
            await MainActor.run {
                Self.actor.getAllSpaces()
            }
        }

        func getSpacesForWindow(windowID: CGWindowID) async -> [SpaceInfo] {
            await MainActor.run {
                Self.actor.getSpacesForWindow(windowID: windowID)
            }
        }

        func moveWindowToCurrentSpace(windowID: CGWindowID) async throws {
            try await MainActor.run {
                try Self.actor.moveWindowToCurrentSpace(windowID: windowID)
            }
        }

        func moveWindowToSpace(windowID: CGWindowID, spaceID: CGSSpaceID) async throws {
            try await MainActor.run {
                try Self.actor.moveWindowToSpace(windowID: windowID, spaceID: spaceID)
            }
        }

        func switchToSpace(_ spaceID: CGSSpaceID) async throws {
            try await Self.actor.switchToSpace(spaceID)
        }
    }

    @MainActor
    private final class SpaceManagementActor {
        private let inner = SpaceManagementService()

        func getAllSpaces() -> [SpaceInfo] {
            self.inner.getAllSpaces()
        }

        func getSpacesForWindow(windowID: CGWindowID) -> [SpaceInfo] {
            self.inner.getSpacesForWindow(windowID: windowID)
        }

        func moveWindowToCurrentSpace(windowID: CGWindowID) throws {
            try self.inner.moveWindowToCurrentSpace(windowID: windowID)
        }

        func moveWindowToSpace(windowID: CGWindowID, spaceID: CGSSpaceID) throws {
            try self.inner.moveWindowToSpace(windowID: windowID, spaceID: spaceID)
        }

        func switchToSpace(_ spaceID: CGSSpaceID) async throws {
            try await self.inner.switchToSpace(spaceID)
        }
    }
}

/// Manage macOS Spaces (virtual desktops)
@MainActor
struct SpaceCommand: ParsableCommand {
    static let commandDescription = CommandDescription(
        commandName: "space",
        abstract: "Manage macOS Spaces (virtual desktops)",
        discussion: """
        SYNOPSIS:
          peekaboo space SUBCOMMAND [OPTIONS]

        DESCRIPTION:
          Provides Space (virtual desktop) management capabilities including
          listing Spaces, switching between them, and moving windows.

        EXAMPLES:
          # List all Spaces
          peekaboo space list

          # Switch to Space 2
          peekaboo space switch --to 2

          # Move window to Space 3
          peekaboo space move-window --app Safari --to 3

          # Move window to current Space
          peekaboo space move-window --app Terminal --to-current

        SUBCOMMANDS:
          list          List all Spaces and their windows
          switch        Switch to a different Space
          move-window   Move a window to a different Space

        NOTE:
          Space management uses private macOS APIs that may change between
          macOS versions. Some features may not work on all systems.
        """,
        subcommands: [
            ListSubcommand.self,
            SwitchSubcommand.self,
            MoveWindowSubcommand.self,
        ],
        showHelpOnEmptyInvocation: true
    )
}

// MARK: - List Spaces

@MainActor

struct ListSubcommand: ErrorHandlingCommand, OutputFormattable {
    @Flag(name: .long, help: "Include detailed window information")
    var detailed = false
    @RuntimeStorage private var runtime: CommandRuntime?

    private var resolvedRuntime: CommandRuntime {
        guard let runtime else {
            preconditionFailure("CommandRuntime must be configured before accessing runtime resources")
        }
        return runtime
    }

    private var services: any PeekabooServiceProviding { self.resolvedRuntime.services }
    private var logger: Logger { self.resolvedRuntime.logger }
    var outputLogger: Logger { self.logger }
    var jsonOutput: Bool { self.resolvedRuntime.configuration.jsonOutput }

    @MainActor
    mutating func run(using runtime: CommandRuntime) async throws {
        self.runtime = runtime
        self.logger.setJsonOutputMode(self.jsonOutput)

        let spaceService = SpaceCommandEnvironment.service
        let spaces = await spaceService.getAllSpaces()
        AutomationEventLogger.log(
            .space,
            "list count=\(spaces.count) detailed=\(self.detailed ? 1 : 0)"
        )

        if self.jsonOutput {
            let data = SpaceListData(
                spaces: spaces.map { space in
                    SpaceData(
                        id: space.id,
                        type: space.type.rawValue,
                        is_active: space.isActive,
                        display_id: space.displayID
                    )
                }
            )
            outputSuccessCodable(data: data, logger: self.logger)
            return
        }

        print("Spaces:")
        var windowsBySpace: [UInt64: [(app: String, window: ServiceWindowInfo)]] = [:]

        if self.detailed {
            let appService = self.services.applications
            let appListResult = try await appService.listApplications()

            for app in appListResult.data.applications where app.windowCount > 0 {
                do {
                    let windowsResult = try await appService.listWindows(for: app.name, timeout: nil)
                    for window in windowsResult.data.windows {
                        let windowSpaces = await spaceService.getSpacesForWindow(windowID: CGWindowID(window.windowID))
                        for space in windowSpaces {
                            windowsBySpace[space.id, default: []].append((app: app.name, window: window))
                        }
                    }
                } catch {
                    continue
                }
            }
        }

        for (index, space) in spaces.indexed() {
            let marker = space.isActive ? "→" : " "
            let displayInfo = space.displayID.map { " (Display \($0))" } ?? ""
            print("\(marker) Space \(index + 1) [ID: \(space.id), Type: \(space.type.rawValue)\(displayInfo)]")

            if self.detailed {
                if let windows = windowsBySpace[space.id], !windows.isEmpty {
                    for (app, window) in windows {
                        let title = window.title.isEmpty ? "[Untitled]" : window.title
                        let minimized = window.isMinimized ? " [MINIMIZED]" : ""
                        print("    • \(app): \(title)\(minimized)")
                    }
                } else {
                    print("    (No windows)")
                }
            }
        }

        if spaces.isEmpty {
            print("No Spaces found (this may indicate an API issue)")
        }
    }
}

// MARK: - Switch Space

@MainActor

struct SwitchSubcommand: ErrorHandlingCommand, OutputFormattable {
    @Option(name: .long, help: "Space number to switch to (1-based)")
    var to: Int
    @RuntimeStorage private var runtime: CommandRuntime?

    private var resolvedRuntime: CommandRuntime {
        guard let runtime else {
            preconditionFailure("CommandRuntime must be configured before accessing runtime resources")
        }
        return runtime
    }

    private var logger: Logger { self.resolvedRuntime.logger }
    var outputLogger: Logger { self.logger }
    var jsonOutput: Bool { self.resolvedRuntime.configuration.jsonOutput }

    /// Validate the requested Space index, switch to it, and report the outcome.
    @MainActor
    mutating func run(using runtime: CommandRuntime) async throws {
        self.runtime = runtime
        self.logger.setJsonOutputMode(self.jsonOutput)

        do {
            let spaceService = SpaceCommandEnvironment.service
            let spaces = await spaceService.getAllSpaces()

            guard self.to > 0 && self.to <= spaces.count else {
                throw ValidationError("Invalid Space number. Available: 1-\(spaces.count)")
            }

            let targetSpace = spaces[self.to - 1]
            try await spaceService.switchToSpace(targetSpace.id)
            AutomationEventLogger.log(
                .space,
                "switch to=\(self.to) space_id=\(targetSpace.id)"
            )

            if self.jsonOutput {
                let data = SpaceActionResult(
                    action: "switch",
                    success: true,
                    space_id: targetSpace.id,
                    space_number: self.to
                )
                outputSuccessCodable(data: data, logger: self.logger)
            } else {
                print("✓ Switched to Space \(self.to)")
            }

        } catch {
            handleError(error)
            throw ExitCode(1)
        }
    }
}

// MARK: - Move Window to Space

@MainActor

struct MoveWindowSubcommand: ApplicationResolvable, ErrorHandlingCommand, OutputFormattable {
    @Option(name: .long, help: "Target application name, bundle ID, or 'PID:12345'")
    var app: String?

    @Option(name: .long, help: "Target application by process ID")
    var pid: Int32?

    @Option(name: .long, help: "Target window by title (partial match supported)")
    var windowTitle: String?

    @Option(name: .long, help: "Target window by index (0-based, frontmost is 0)")
    var windowIndex: Int?

    @Option(name: .long, help: "Space number to move window to (1-based)")
    var to: Int?

    @Flag(name: .long, help: "Move window to current Space")
    var toCurrent = false

    @Flag(name: .long, help: "Switch to the target Space after moving")
    var follow = false
    @RuntimeStorage private var runtime: CommandRuntime?

    private var resolvedRuntime: CommandRuntime {
        guard let runtime else {
            preconditionFailure("CommandRuntime must be configured before accessing runtime resources")
        }
        return runtime
    }

    private var services: any PeekabooServiceProviding { self.resolvedRuntime.services }
    private var logger: Logger { self.resolvedRuntime.logger }
    var outputLogger: Logger { self.logger }
    var jsonOutput: Bool { self.resolvedRuntime.configuration.jsonOutput }

    @MainActor
    mutating func run(using runtime: CommandRuntime) async throws {
        self.runtime = runtime
        self.logger.setJsonOutputMode(self.jsonOutput)

        do {
            let appIdentifier = try self.resolveApplicationIdentifier()

            guard self.to != nil || self.toCurrent else {
                throw ValidationError("Must specify either --to or --to-current")
            }
            guard !(self.to != nil && self.toCurrent) else {
                throw ValidationError("Cannot specify both --to and --to-current")
            }

            var windowOptions = WindowIdentificationOptions()
            windowOptions.app = appIdentifier
            windowOptions.windowTitle = self.windowTitle
            windowOptions.windowIndex = self.windowIndex

            let target = try windowOptions.toWindowTarget()
            let windows = try await self.services.windows.listWindows(target: target)
            guard let windowInfo = windowOptions.selectWindow(from: windows) else {
                throw NotFoundError.window(app: appIdentifier)
            }

            let windowID = CGWindowID(windowInfo.windowID)
            let spaceService = SpaceCommandEnvironment.service

            if self.toCurrent {
                try await spaceService.moveWindowToCurrentSpace(windowID: windowID)
                AutomationEventLogger.log(
                    .space,
                    "move_window window_id=\(windowID) mode=current title=\"\(windowInfo.title)\""
                )
                if self.jsonOutput {
                    let data = WindowSpaceActionResult(
                        action: "move-window",
                        success: true,
                        window_id: windowID,
                        window_title: windowInfo.title,
                        space_id: nil,
                        space_number: nil,
                        moved_to_current: true,
                        followed: nil
                    )
                    outputSuccessCodable(data: data, logger: self.logger)
                } else {
                    print("✓ Moved window '\(windowInfo.title)' to current Space")
                }
                return
            }

            guard let spaceNum = self.to else {
                preconditionFailure("Expected either --to or --to-current validation")
            }

            let spaces = await spaceService.getAllSpaces()
            guard spaceNum > 0 && spaceNum <= spaces.count else {
                throw ValidationError("Invalid Space number. Available: 1-\(spaces.count)")
            }

            let targetSpace = spaces[spaceNum - 1]
            try await spaceService.moveWindowToSpace(windowID: windowID, spaceID: targetSpace.id)
            if self.follow {
                try await spaceService.switchToSpace(targetSpace.id)
            }
            AutomationEventLogger.log(
                .space,
                "move_window window_id=\(windowID) space=\(spaceNum) follow=\(self.follow ? 1 : 0) "
                    + "title=\"\(windowInfo.title)\""
            )

            if self.jsonOutput {
                let data = WindowSpaceActionResult(
                    action: "move-window",
                    success: true,
                    window_id: windowID,
                    window_title: windowInfo.title,
                    space_id: targetSpace.id,
                    space_number: spaceNum,
                    moved_to_current: false,
                    followed: self.follow
                )
                outputSuccessCodable(data: data, logger: self.logger)
            } else {
                var message = "✓ Moved window '\(windowInfo.title)' to Space \(spaceNum)"
                if self.follow { message += " (and switched to it)" }
                print(message)
            }
        } catch {
            handleError(error)
            throw ExitCode(1)
        }
    }
}

@MainActor
extension ListSubcommand: ParsableCommand {
    nonisolated(unsafe) static var commandDescription: CommandDescription {
        MainActorCommandDescription.describe {
            CommandDescription(
                commandName: "list",
                abstract: "List all Spaces and their windows"
            )
        }
    }
}

extension ListSubcommand: AsyncRuntimeCommand {}
@MainActor
extension ListSubcommand: CommanderBindableCommand {
    mutating func applyCommanderValues(_ values: CommanderBindableValues) throws {
        self.detailed = values.flag("detailed")
    }
}

@MainActor
extension SwitchSubcommand: ParsableCommand {
    nonisolated(unsafe) static var commandDescription: CommandDescription {
        MainActorCommandDescription.describe {
            CommandDescription(
                commandName: "switch",
                abstract: "Switch to a different Space"
            )
        }
    }
}

extension SwitchSubcommand: AsyncRuntimeCommand {}
@MainActor
extension SwitchSubcommand: CommanderBindableCommand {
    mutating func applyCommanderValues(_ values: CommanderBindableValues) throws {
        self.to = try values.requireOption("to", as: Int.self)
    }
}

@MainActor
extension MoveWindowSubcommand: ParsableCommand {
    nonisolated(unsafe) static var commandDescription: CommandDescription {
        MainActorCommandDescription.describe {
            CommandDescription(
                commandName: "move-window",
                abstract: "Move a window to a different Space"
            )
        }
    }
}

extension MoveWindowSubcommand: AsyncRuntimeCommand {}
@MainActor
extension MoveWindowSubcommand: CommanderBindableCommand {
    mutating func applyCommanderValues(_ values: CommanderBindableValues) throws {
        self.app = values.singleOption("app")
        self.pid = try values.decodeOption("pid", as: Int32.self)
        self.windowTitle = values.singleOption("windowTitle")
        self.windowIndex = try values.decodeOption("windowIndex", as: Int.self)
        self.to = try values.decodeOption("to", as: Int.self)
        self.toCurrent = values.flag("toCurrent")
        self.follow = values.flag("follow")
    }
}

// MARK: - Response Types

struct SpaceListData: Codable {
    let spaces: [SpaceData]
}

struct SpaceData: Codable {
    let id: UInt64
    let type: String
    let is_active: Bool
    let display_id: CGDirectDisplayID?
}

struct SpaceActionResult: Codable {
    let action: String
    let success: Bool
    let space_id: UInt64
    let space_number: Int
}

struct WindowSpaceActionResult: Codable {
    let action: String
    let success: Bool
    let window_id: CGWindowID
    let window_title: String
    let space_id: UInt64?
    let space_number: Int?
    let moved_to_current: Bool?
    let followed: Bool?
}
